昨天提到關於「DRY 原則」(Don't Repeat Yourself),要避免重複程式碼。但是有一個看似矛盾的觀念叫做「Write Everything Twice」,簡稱 WET 原則,第一次聽到 WET 原則以為是在搞笑😂,殊不知真的有人在談論他,來觀看英文維基百科對於 WET 原則描述如下
The opposing view to DRY is called WET, a backronym commonly taken to stand for write everything twice (alternatively write every time, we enjoy typing or waste everyone's time). WET solutions are common in multi-tiered architectures where a developer may be tasked with, for example, adding a comment field on a form in a web application. The text string "comment" might be repeated in the label, the HTML tag, in a read function name, a private variable, database DDL, queries, and so on. A DRY approach eliminates that redundancy by using frameworks that reduce or eliminate all those editing tasks except the most important ones, leaving the extensibility of adding new knowledge variables in one place.This conceptualization of "WET" as an alternative to "DRY" programming has been around since at least 2002 in the Java world, though it is not known who coined the term.
在中文的 wiki 對於 WET 原則的介紹來自於 The Pragmatic Programmer 書中認為重複可以發生在以下幾種情況
連知名大大 Kent C. Dodds 也闡述了對於 WET 原則,大概的意思不好的抽象比起重複的程式碼更糟糕,他在文章中還提到了 AHA(發音為啊哈),套用 WET 原則取 Write Everything Twice 的每個字母第一個字,AHA 則是"Avoid Hasty Abstractions"
換句話說,這不是鼓勵我們寫出重複的程式碼,而是提醒我們:過早的抽象化比重複程式碼更危險。接下來我們會透過 TypeScript 範例來了解這個概念。
以下用同一個「結帳金額計算」業務邏輯做對照:過度 DRY(不良) vs WET(較佳)。兩段程式皆為 React + TypeScript
import React from "react";
type Context = "cart" | "order";
type BadProps = {
subtotal: number;
context: Context;
vip?: boolean;
coupon?: number;
rush?: boolean;
showInvoiceFee?: boolean;
};
const BadPriceCard: React.FC<BadProps> = (props) => {
let shipping = props.subtotal < 1000 ? 60 : 0;
let discount = 0;
if (props.context === "cart") {
if (props.coupon) discount += props.coupon;
if (props.vip) discount += Math.min(100, props.subtotal * 0.05);
if (props.rush) shipping += 120; // 其實購物車不該出現加急邏輯
} else {
// order
if (props.vip) discount += 50; // 與 cart 不同規則糾纏在一起
if (props.subtotal > 5000) shipping = 0;
if (props.rush) shipping += 120;
}
const invoiceFee = props.showInvoiceFee && props.context === "order" ? 20 : 0;
const total = Math.max(0, props.subtotal + shipping + invoiceFee - discount);
return (
<div style={{ border: "1px solid #ccc", padding: 12 }}>
<h3>{props.context === "cart" ? "購物車摘要" : "訂單明細"}</h3>
<div>小計:{props.subtotal}</div>
{props.coupon ? <div>折價券:-{props.coupon}</div> : null}
{props.vip ? <div>VIP 折抵:已套用</div> : null}
{props.showInvoiceFee && props.context === "order" ? <div>發票處理費:{invoiceFee}</div> : null}
{props.rush ? <div>急件處理:已加收</div> : null}
<div>運費:{shipping}</div>
<strong>應付:{total}</strong>
</div>
);
};
export const BadCart = () => (
<BadPriceCard subtotal={1200} context="cart" vip coupon={100} rush />
);
export const BadOrder = () => (
<BadPriceCard subtotal={700} context="order" rush showInvoiceFee />
);
import React from "react";
type CartProps = { subtotal: number; vip?: boolean; coupon?: number };
type OrderProps = { subtotal: number; vip?: boolean; rush?: boolean };
export const CartSummary: React.FC<CartProps> = ({ subtotal, vip, coupon }) => {
const shipping = subtotal < 1000 ? 60 : 0;
const discount = (coupon ?? 0) + (vip ? Math.min(100, subtotal * 0.05) : 0);
const total = Math.max(0, subtotal + shipping - discount);
return (
<div style={{ border: "1px solid #8bc", padding: 12 }}>
<h3>購物車摘要</h3>
<div>小計:{subtotal}</div>
{coupon ? <div>折價券:-{coupon}</div> : null}
{vip ? <div>VIP 折抵:已套用</div> : null}
<div>運費:{shipping}</div>
<strong>應付:{total}</strong>
</div>
);
};
export const OrderSummary: React.FC<OrderProps> = ({ subtotal, vip, rush }) => {
const baseShipping = subtotal > 5000 ? 0 : 60;
const shipping = rush ? baseShipping + 120 : baseShipping;
const discount = vip ? 50 : 0;
const invoiceFee = 20;
const total = Math.max(0, subtotal + shipping + invoiceFee - discount);
return (
<div style={{ border: "1px solid #8c8", padding: 12 }}>
<h3>訂單明細</h3>
<div>小計:{subtotal}</div>
{vip ? <div>VIP 折抵:-{discount}</div> : null}
{rush ? <div>急件處理:+120</div> : null}
<div>發票處理費:{invoiceFee}</div>
<div>運費:{shipping}</div>
<strong>應付:{total}</strong>
</div>
);
};
export const GoodDemo = () => (
<>
<CartSummary subtotal={1200} vip coupon={100} />
<OrderSummary subtotal={700} rush />
</>
);
if/else 與旗標(context、rush、showInvoiceFee…),抽象錯誤、耦合緊、難讀難改。「Write Everything Twice」的核心概念提醒我們:
這不是說永遠不要抽象化,而是要在正確的時機進行抽象化。當我們真正理解問題域,並且看到明確的重複模式時,才是提取共用邏輯的最佳時機。
在現代開發中,WET 原則特別適用於快速變化的業務需求階段。這時候我們可以發現,讓程式碼先「活著」比讓它「完美」更重要。等需求穩定後,再進行適當的重構和抽象化,往往能得到更好的架構設計。